Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.75% covered (success)
93.75%
180 / 192
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddDoctrineFields
93.75% covered (success)
93.75%
180 / 192
50.00% covered (danger)
50.00%
2 / 4
52.66
0.00% covered (danger)
0.00%
0 / 1
 postRun
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 applyId
86.21% covered (warning)
86.21%
50 / 58
0.00% covered (danger)
0.00%
0 / 1
19.95
 iterateProperties
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 patch
96.83% covered (success)
96.83%
122 / 126
0.00% covered (danger)
0.00%
0 / 1
26
1<?php
2namespace Apie\DoctrineEntityConverter\CodeGenerators;
3
4use Apie\Core\Context\ApieContext;
5use Apie\Core\Identifiers\AutoIncrementInteger;
6use Apie\Core\Metadata\MetadataFactory;
7use Apie\Core\Utils\ConverterUtils;
8use Apie\DoctrineEntityConverter\Concerns\HasGeneralDoctrineFields;
9use Apie\DoctrineEntityConverter\Entities\SearchIndex;
10use Apie\StorageMetadata\Attributes\AclLinkAttribute;
11use Apie\StorageMetadata\Attributes\DiscriminatorMappingAttribute;
12use Apie\StorageMetadata\Attributes\GetMethodAttribute;
13use Apie\StorageMetadata\Attributes\GetSearchIndexAttribute;
14use Apie\StorageMetadata\Attributes\ManyToOneAttribute;
15use Apie\StorageMetadata\Attributes\OneToManyAttribute;
16use Apie\StorageMetadata\Attributes\OneToOneAttribute;
17use Apie\StorageMetadata\Attributes\OrderAttribute;
18use Apie\StorageMetadata\Attributes\ParentAttribute;
19use Apie\StorageMetadata\Attributes\PropertyAttribute;
20use Apie\StorageMetadata\Interfaces\AutoIncrementTableInterface;
21use Apie\StorageMetadataBuilder\Interfaces\MixedStorageInterface;
22use Apie\StorageMetadataBuilder\Interfaces\PostRunGeneratedCodeContextInterface;
23use Apie\StorageMetadataBuilder\Mediators\GeneratedCodeContext;
24use Apie\TypeConverter\ReflectionTypeFactory;
25use Doctrine\Common\Collections\Collection;
26use Doctrine\ORM\Mapping\Column;
27use Doctrine\ORM\Mapping\Entity;
28use Doctrine\ORM\Mapping\GeneratedValue;
29use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
30use Doctrine\ORM\Mapping\Id;
31use Doctrine\ORM\Mapping\JoinColumn;
32use Doctrine\ORM\Mapping\ManyToMany;
33use Doctrine\ORM\Mapping\ManyToOne;
34use Doctrine\ORM\Mapping\OneToMany;
35use Doctrine\ORM\Mapping\OneToOne;
36use Doctrine\ORM\Mapping\OrderBy;
37use Generator;
38use Nette\PhpGenerator\Attribute;
39use Nette\PhpGenerator\ClassType;
40use Nette\PhpGenerator\PromotedParameter;
41use Nette\PhpGenerator\Property;
42use ReflectionClass;
43use ReflectionProperty;
44
45/**
46 * Adds created_at and updated_at and Doctrine attributes
47 */
48class AddDoctrineFields implements PostRunGeneratedCodeContextInterface
49{
50    public function postRun(GeneratedCodeContext $generatedCodeContext): void
51    {
52        foreach ($generatedCodeContext->generatedCode->generatedCodeHashmap as $code) {
53            $this->patch($generatedCodeContext, $code);
54        }
55    }
56
57    private function applyId(ClassType $classType): void
58    {
59        $property = null;
60        $doctrineType = null;
61        $nullable = false;
62        $generatedValue = false;
63        if ($classType->hasProperty('id')) {
64            $property = $classType->getProperty('id');
65        } elseif ($classType->hasProperty('search_id')) {
66            $property = $classType->getProperty('search_id')->cloneWithName('id');
67            $classType->addMember($property);
68        }
69        if ($property === null) {
70            $property = $classType->addProperty('id')->setType('?int');
71            $generatedValue = true;
72            $doctrineType = 'integer';
73        } else {
74            // @see ClassTypeFactory
75            $originalClass = $classType->getComment();
76            if ($originalClass && class_exists($originalClass)) {
77                $metadata = MetadataFactory::getResultMetadata(
78                    new ReflectionClass($originalClass),
79                    new ApieContext()
80                );
81                $hashmap = $metadata->getHashmap();
82                if (isset($hashmap['id'])) {
83                    $type = $hashmap['id']->getTypehint();
84                    $nullable = $hashmap['id']->allowsNull();
85                    $class = ConverterUtils::toReflectionClass($type);
86                    if ($class && $class->isSubclassOf(AutoIncrementInteger::class)) {
87                        $generatedValue = true;
88                        $nullable = false;
89                        $property->setInitialized(true);
90                    }
91                    $scalarType = MetadataFactory::getScalarForType($hashmap['id']->getTypehint(), true);
92                    $property->setType(
93                        $scalarType->value
94                    );
95                    $doctrineType = $scalarType->toDoctrineType();
96                }
97            }
98        }
99
100        if (in_array(AutoIncrementTableInterface::class, $classType->getImplements())
101            || in_array(MixedStorageInterface::class, $classType->getImplements())) {
102            $generatedValue = true;
103            $nullable = false;
104        }
105
106        $hasIdAttribute = false;
107        $hasColumnAttribute = false;
108        foreach ($property->getAttributes() as $attribute) {
109            if (in_array($attribute->getName(), [Column::class, ManyToOne::class, OneToMany::class, ManyToMany::class])) {
110                $hasColumnAttribute = true;
111                break;
112            }
113            if ($attribute->getName() === GeneratedValue::class) {
114                $generatedValue = false;
115            }
116            if ($attribute->getName() === Id::class) {
117                $hasIdAttribute = true;
118            }
119        }
120        if (!$hasIdAttribute) {
121            $property->addAttribute(Id::class);
122        }
123        if (!$hasColumnAttribute) {
124            if ($doctrineType === null) {
125                $doctrineType = MetadataFactory::getScalarForType(
126                    ReflectionTypeFactory::createReflectionType($property->getType()),
127                    true
128                )->toDoctrineType();
129            }
130            $property->addAttribute(Column::class, ['type' => $doctrineType, 'nullable' => $nullable]);
131        }
132        if ($generatedValue) {
133            $property->addAttribute(GeneratedValue::class);
134        }
135    }
136
137    /**
138     * @return Generator<int, PromotedParameter|Property>
139     */
140    private function iterateProperties(ClassType $classType): Generator
141    {
142        foreach ($classType->getProperties() as $property) {
143            yield $property;
144        }
145        if ($classType->hasMethod('__construct')) {
146            foreach ($classType->getMethod('__construct')->getParameters() as $parameter) {
147                if ($parameter instanceof PromotedParameter) {
148                    yield $parameter;
149                }
150            }
151        }
152    }
153
154    private function patch(GeneratedCodeContext $generatedCodeContext, ClassType $classType): void
155    {
156        $classType->addAttribute(Entity::class);
157        $classType->addAttribute(HasLifecycleCallbacks::class);
158        $classType->addTrait('\\' . HasGeneralDoctrineFields::class);
159
160        foreach ($this->iterateProperties($classType) as $property) {
161            $added = false;
162            foreach ($property->getAttributes() as $attribute) {
163                switch ($attribute->getName()) {
164                    case GetMethodAttribute::class:
165                    case PropertyAttribute::class:
166                        $added = true;
167                        if (in_array($property->getType(), ['DateTimeImmutable', '?DateTimeImmutable'])) {
168                            $property->addAttribute(Column::class, ['nullable' => true, 'type' => 'datetimetz_immutable']);
169                        } else {
170                            $arguments = $attribute->getArguments();
171                            if ($arguments[2] ?? false) {
172                                $property->addAttribute(Column::class, ['nullable' => true, 'type' => 'text', 'length' => 65535]);
173                            } else {
174                                $property->addAttribute(Column::class, ['nullable' => true]);
175                            }
176                        }
177                        break;
178                    case DiscriminatorMappingAttribute::class:
179                        $added = true;
180                        $property->addAttribute(Column::class, ['type' => 'json']);
181                        break;
182                    case ManyToOneAttribute::class:
183                        $added = true;
184                        $targetEntity = $property->getType();
185                        $property->addAttribute(
186                            ManyToOne::class,
187                            [
188                                'targetEntity' => $targetEntity,
189                                'inversedBy' => $attribute->getArguments()[0],
190                            ]
191                        );
192                        $property->addAttribute(
193                            JoinColumn::class,
194                            [
195                                'nullable' => true,
196                            ]
197                        );
198                        break;
199                    case OneToManyAttribute::class:
200                    case AclLinkAttribute::class:
201                        $added = true;
202                        $property->setType(Collection::class);
203                        if ($attribute->getName() === OneToManyAttribute::class) {
204                            $targetEntity = $attribute->getArguments()[1];
205                            $mappedByProperty = $generatedCodeContext->findParentProperty($targetEntity);
206                            $mappedByProperty ??= $attribute->getArguments()[0];
207                            $mappedByProperty ??= 'ref_' . $classType->getName();
208                        } else {
209                            $targetEntity = $attribute->getArguments()[0];
210                            $mappedByProperty = 'ref_' . $classType->getName();
211                        }
212                        $indexByProperty = $generatedCodeContext->findIndexProperty($targetEntity);
213                        if ($indexByProperty) {
214                            $property->addAttribute(OrderBy::class, [[$indexByProperty => 'ASC']]);
215                        }
216                        $property->addAttribute(
217                            OneToMany::class,
218                            [
219                                'cascade' => ['all'],
220                                'targetEntity' => $targetEntity,
221                                'mappedBy' => $mappedByProperty,
222                                'fetch' => 'EAGER',
223                                'indexBy' => $indexByProperty,
224                                'orphanRemoval' => true,
225                            ]
226                        );
227                        break;
228                    case OneToOneAttribute::class:
229                        $added = true;
230                        $targetEntity = $property->getType();
231                        // look for @ParentAttribute for inversedBy?
232                        $property->addAttribute(
233                            OneToOne::class,
234                            [
235                                'cascade' => ['all'],
236                                'targetEntity' => $targetEntity,
237                                'fetch' => 'EAGER',
238                            ]
239                        );
240                        break;
241                    case GetSearchIndexAttribute::class:
242                        $added = true;
243                        $property->setType(Collection::class);
244                        $searchTableName = strpos($classType->getName(), 'apie_resource__') === 0
245                            ? preg_replace('/^apie_resource__/', 'apie_index__', $classType->getName())
246                            : 'apie_index__' . $classType->getName();
247                        $searchTableName .= '_' . $property->getName();
248                        $searchTable = SearchIndex::createFor(
249                            $searchTableName,
250                            $classType->getName(),
251                            $property->getName(),
252                        );
253                        $generatedCodeContext->generatedCode->generatedCodeHashmap[$searchTableName] = $searchTable;
254                        $property->addAttribute(
255                            OneToMany::class,
256                            [
257                                'cascade' => ['all'],
258                                'targetEntity' => $searchTableName,
259                                'mappedBy' => 'parent',
260                                'orphanRemoval' => true,
261                            ]
262                        );
263                        $args = $attribute->getArguments();
264                        $args['arrayValueType'] = $searchTableName;
265                        // there is no good method in nette/php-generator
266                        (new ReflectionProperty(Attribute::class, 'args'))->setValue($attribute, $args);
267                        $type = $property->getType();
268                        break;
269                    case OrderAttribute::class:
270                        $added = true;
271                        $type = 'string';
272                        if ($property->getType() === 'int') {
273                            $type = 'integer';
274                        }
275                        $property->addAttribute(Column::class, ['type' => $type]);
276                        break;
277                    case ParentAttribute::class:
278                        $added = true;
279                        $inversedBy = $generatedCodeContext->findInverseProperty($property->getType());
280                        $property->addAttribute(
281                            ManyToOne::class,
282                            ['targetEntity' => $property->getType(), 'inversedBy' => $inversedBy]
283                        );
284                        break;
285                }
286            }
287            if (!$added) {
288                $type = $property->getType();
289                switch ((string) $type) {
290                    case 'string':
291                        $property->addAttribute(Column::class, ['type' => 'text', 'nullable' => $property->isNullable()]);
292                        break;
293                    case 'float':
294                        $property->addAttribute(Column::class, ['type' => 'float', 'nullable' => $property->isNullable()]);
295                        break;
296                    case 'int':
297                    case '?int':
298                        $property->addAttribute(Column::class, ['type' => 'integer', 'nullable' => $property->isNullable()]);
299                        break;
300                    case 'array':
301                    case '?array':
302                        $property->addAttribute(Column::class, ['type' => 'json', 'nullable' => $property->isNullable()]);
303                        break;
304                }
305            }
306        }
307
308        $this->applyId($classType);
309    }
310}